# Run capa against loaded Ghidra database and render results in Ghidra Console window
# @author Mike Hunhoff (mehunhoff@google.com)
# @category Python 3.capa

# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import sys
import logging
import pathlib
import argparse

import capa
import capa.main
import capa.rules
import capa.ghidra.helpers
import capa.render.default
import capa.capabilities.common
import capa.features.extractors.ghidra.extractor

logger = logging.getLogger("capa_ghidra")


def run_headless():
    parser = argparse.ArgumentParser(description="The FLARE team's open-source tool to integrate capa with Ghidra.")

    parser.add_argument(
        "rules",
        type=str,
        help="path to rule file or directory",
    )
    parser.add_argument(
        "-v", "--verbose", action="store_true", help="enable verbose result document (no effect with --json)"
    )
    parser.add_argument(
        "-vv", "--vverbose", action="store_true", help="enable very verbose result document (no effect with --json)"
    )
    parser.add_argument("-d", "--debug", action="store_true", help="enable debugging output on STDERR")
    parser.add_argument("-q", "--quiet", action="store_true", help="disable all output but errors")
    parser.add_argument("-j", "--json", action="store_true", help="emit JSON instead of text")

    script_args = list(getScriptArgs())  # type: ignore [name-defined] # noqa: F821
    if not script_args or len(script_args) > 1:
        script_args = []
    else:
        script_args = script_args[0].split()
        for idx, arg in enumerate(script_args):
            if arg.lower() == "help":
                script_args[idx] = "--help"

    args = parser.parse_args(args=script_args)

    if args.quiet:
        logging.basicConfig(level=logging.WARNING)
        logging.getLogger().setLevel(logging.WARNING)
    elif args.debug:
        logging.basicConfig(level=logging.DEBUG)
        logging.getLogger().setLevel(logging.DEBUG)
    else:
        logging.basicConfig(level=logging.INFO)
        logging.getLogger().setLevel(logging.INFO)

    logger.debug("running in Ghidra headless mode")

    rules_path = pathlib.Path(args.rules)

    logger.debug("rule path: %s", rules_path)
    rules = capa.rules.get_rules([rules_path])

    meta = capa.ghidra.helpers.collect_metadata([rules_path])
    extractor = capa.features.extractors.ghidra.extractor.GhidraFeatureExtractor()

    capabilities = capa.capabilities.common.find_capabilities(rules, extractor, False)

    meta.analysis.feature_counts = capabilities.feature_counts
    meta.analysis.library_functions = capabilities.library_functions
    meta.analysis.layout = capa.loader.compute_layout(rules, extractor, capabilities.matches)

    if capa.capabilities.common.has_static_limitation(rules, capabilities, is_standalone=True):
        logger.info("capa encountered warnings during analysis")

    if args.json:
        print(capa.render.json.render(meta, rules, capabilities.matches))  # noqa: T201
    elif args.vverbose:
        print(capa.render.vverbose.render(meta, rules, capabilities.matches))  # noqa: T201
    elif args.verbose:
        print(capa.render.verbose.render(meta, rules, capabilities.matches))  # noqa: T201
    else:
        print(capa.render.default.render(meta, rules, capabilities.matches))  # noqa: T201

    return 0


def run_ui():
    logging.basicConfig(level=logging.INFO)
    logging.getLogger().setLevel(logging.INFO)

    rules_dir: str = ""
    try:
        selected_dir = askDirectory("Choose capa rules directory", "Ok")  # type: ignore [name-defined] # noqa: F821
        if selected_dir:
            rules_dir = selected_dir.getPath()
    except RuntimeError:
        # RuntimeError thrown when user selects "Cancel"
        pass

    if not rules_dir:
        logger.info("You must choose a capa rules directory before running capa.")
        return capa.main.E_MISSING_RULES

    verbose = askChoice(  # type: ignore [name-defined] # noqa: F821
        "capa output verbosity", "Choose capa output verbosity", ["default", "verbose", "vverbose"], "default"
    )

    rules_path: pathlib.Path = pathlib.Path(rules_dir)
    logger.info("running capa using rules from %s", str(rules_path))

    rules = capa.rules.get_rules([rules_path])

    meta = capa.ghidra.helpers.collect_metadata([rules_path])
    extractor = capa.features.extractors.ghidra.extractor.GhidraFeatureExtractor()

    capabilities = capa.capabilities.common.find_capabilities(rules, extractor, True)

    meta.analysis.feature_counts = capabilities.feature_counts
    meta.analysis.library_functions = capabilities.library_functions
    meta.analysis.layout = capa.loader.compute_layout(rules, extractor, capabilities.matches)

    if capa.capabilities.common.has_static_limitation(rules, capabilities, is_standalone=False):
        logger.info("capa encountered warnings during analysis")

    if verbose == "vverbose":
        print(capa.render.vverbose.render(meta, rules, capabilities.matches))  # noqa: T201
    elif verbose == "verbose":
        print(capa.render.verbose.render(meta, rules, capabilities.matches))  # noqa: T201
    else:
        print(capa.render.default.render(meta, rules, capabilities.matches))  # noqa: T201

    return 0


def main():
    if not capa.ghidra.helpers.is_supported_ghidra_version():
        return capa.main.E_UNSUPPORTED_GHIDRA_VERSION

    if not capa.ghidra.helpers.is_supported_file_type():
        return capa.main.E_INVALID_FILE_TYPE

    if not capa.ghidra.helpers.is_supported_arch_type():
        return capa.main.E_INVALID_FILE_ARCH

    if isRunningHeadless():  # type: ignore [name-defined] # noqa: F821
        return run_headless()
    else:
        return run_ui()


if __name__ == "__main__":
    if sys.version_info < (3, 10):
        from capa.exceptions import UnsupportedRuntimeError

        raise UnsupportedRuntimeError("This version of capa can only be used with Python 3.10+")
    sys.exit(main())
